// License Notification
//======================================================
// Copyright (c) 2010 Channel Intelligence Inc.
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// which accompanies this distribution, and is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// ======================================================




using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using CIP4.Common;
using CIP4.Item;

namespace CIP4.Serializer
{
    public abstract class SerializerBuilderBaseType
    {
        public static Type GetActualType(Type t)
        {
            Type actualType = t;
            if (t.IsArray)
                actualType = t.GetElementType();

            if (actualType == null)
                throw new ApplicationException("Unexpected null actual type");

            if (Nullable.GetUnderlyingType(actualType) != null)
                actualType = Nullable.GetUnderlyingType(actualType);

            if (actualType.IsGenericType)
            {
                Type[] types = actualType.GetGenericArguments();
                actualType = types[types.Length - 1];
            }
            return actualType;
        }

        public TypeContextType TheType { get; set; }

        public string GetSerializeAs(PropertyInfo prop)
        {
            CIP4PropertyAttribute att = CIP4PropertyAttribute.GetAttribute(prop);
            if (att != null && !string.IsNullOrEmpty(att.SerializeAs))
                return att.SerializeAs;

            return prop.Name;
        }

        public Dictionary<string, TypeContextType> UsedTypes { get; set; }

        protected Dictionary<string, bool> TypeInclusions = new Dictionary<string, bool>();

        protected Dictionary<string, bool> PropertyInclusions = new Dictionary<string, bool>();

        protected PropertyInfo VersionProperty
        {
            get
            {
                return TheType.VersionProperty;
            }

        }

        public void IncludeTheseTypes(List<string> typeList)
        {
            foreach (string typeName in typeList)
                TypeInclusions.Add(typeName, true);
        }

        protected bool IncludeThisType(Type aType)
        {
            if (aType.Name.StartsWith("CIP4ScalablePropertyType"))
                return false;
            if (TypeInclusions.ContainsKey(aType.Namespace + "." + aType.Name)) // generic type parameters don't have a full name - don't ask me why not...
                return TypeInclusions[aType.Namespace + "." + aType.Name];
            CIP4ItemAttribute att = CIP4ItemAttribute.GetAttribute(aType);
            if (att != null && !att.DontSerialize)
                TypeInclusions.Add(aType.Namespace + "." + aType.Name, true);
            else
                TypeInclusions.Add(aType.Namespace + "." + aType.Name, false);
            return TypeInclusions[aType.Namespace + "." + aType.Name];
        }

        protected bool VersionThisType(Type aType)
        {
            if (ValueType(aType))
                return true;
            Type actualType = GetActualType(aType);
            CIP4ItemAttribute att = CIP4ItemAttribute.GetAttribute(actualType);
            if (att != null && !att.DontSerialize)
            {
                return actualType.GetProperties().Any(t => t.PropertyType.Name == typeof(CIP4ItemVersionsType).Name);
            }
            return false;
        }

        internal bool IncludeThisProperty(PropertyInfo prop)
        {
            if (prop.DeclaringType.GetInterface("IList`1") != null && prop.Name == "Item")
                return false;
            if (PropertyInclusions.ContainsKey(prop.DeclaringType.FullName + "." + prop.Name))
                return PropertyInclusions[prop.DeclaringType.FullName + "." + prop.Name];
            CIP4PropertyAttribute att = CIP4PropertyAttribute.GetAttribute(prop);
            if (string.IsNullOrEmpty(prop.PropertyType.FullName))
                throw new ApplicationException("Unexpected null property type name");
            if (prop.PropertyType.Name == "CIP4ItemVersionsType")
                return false;
            if (att == null || !att.DontSerialize)
            {
                if (att != null && att.SerializeTarget)
                {
                    if (!TypeInclusions.ContainsKey(prop.PropertyType.FullName))
                        TypeInclusions.Add(prop.PropertyType.FullName, true);
                    else
                        TypeInclusions[prop.DeclaringType.FullName] = true;
                }
                PropertyInclusions.Add(prop.DeclaringType.FullName + "." + prop.Name, true);
            }
            else
                PropertyInclusions.Add(prop.DeclaringType.FullName + "." + prop.Name, false);
            return PropertyInclusions[prop.DeclaringType.FullName + "." + prop.Name];
        }

        protected IEnumerable<TypeContextType> TargetTypes(TypeContextType target)
        {
            if (!UsedTypes.ContainsKey(target.TheTypeFullName))
                yield return target;
            foreach (Type t in target.TheType.Assembly.GetTypes())
            {
                if (!target.TheType.IsAssignableFrom(t))
                    continue;
                TypeContextType tc = new TypeContextType { OriginalType = t };
                if (!UsedTypes.ContainsKey(tc.TheTypeFullName))
                    yield return tc;
            }
            yield break;
        }
        
        protected static List<PropertyInfo> GetExternalIdProperties(TypeContextType aType)
        {
            HashSet<int> rankSet = new HashSet<int> ();     // for detecting duplicate ranks
            Dictionary<int, PropertyInfo> rankedProperties = new Dictionary<int, PropertyInfo>();
            List<string> itemAttributeExternalIDs =  
                aType.ItemAttribute == null? new List<string>() : 
                new List<string>( aType.ItemAttribute.ExternalIds ?? new string[] { } );

            int rank = 0;
            foreach (PropertyInfo prop in WrapperSerializerType.SortedProperties(aType.TheType.GetProperties()).Values)
            {
                if (itemAttributeExternalIDs.Contains(prop.Name))
                    rankedProperties.Add(++rank, prop);
            }

            if (rankedProperties.Count != itemAttributeExternalIDs.Count)
                throw new ArgumentException(string.Format("The properties in the CIP4Item attribute ExternalIDs property do not match the declared properties in {0}", aType.TheTypeFullName));

            foreach (PropertyInfo prop in WrapperSerializerType.SortedProperties(aType.TheType.GetProperties()).Values)
            {
                CIP4ExternalIDAttribute attribute = CIP4ExternalIDAttribute.GetAttribute (prop);
                if (attribute != null)
                {
                    if (attribute.Rank < 0)
                        throw new ArgumentException (string.Format ("Rank {0} on {1}.{2} is not valid: must be a non-negative integer.", attribute.Rank, aType.TheTypeFullName, prop.Name));

                    if (!rankSet.Add (attribute.Rank))
                        throw new ArgumentException(string.Format("The rank value {0} on {1}.{2} appears more than once. This makes the sort order ambiguous.", attribute.Rank, aType.TheTypeFullName, prop.Name));

                    if (itemAttributeExternalIDs.Contains(prop.Name) && ((itemAttributeExternalIDs.IndexOf(prop.Name) + 1) != attribute.Rank))
                        throw new ArgumentException(string.Format("Properties mentioned as externalIDs in the CIP4Item Attribute ExternalIDs property must have the same rank as properties that have an CIP4ExternalID attribute in class {0}", aType.TheTypeFullName));

                    if (!rankedProperties.ContainsKey(attribute.Rank))
                        rankedProperties.Add(attribute.Rank, prop);
                    else if (rankedProperties[attribute.Rank] != prop)
                        throw new ArgumentException(string.Format("The ExternalID property {0} has the same rank ({2}) as the ExternalID property {1}", aType.TheTypeFullName + "." + prop.Name, rankedProperties[rank].Name, attribute.Rank));
                }
            }

            List<PropertyInfo> orderedProperties = rankedProperties.OrderBy(kvpi => kvpi.Key).Select(kvp => kvp.Value).ToList();

            List<Type> checkIfMixedUp = new List<Type>();

            foreach (PropertyInfo orderedProperty in orderedProperties)
                if (checkIfMixedUp.Contains(orderedProperty.PropertyType) && checkIfMixedUp[0] != orderedProperty.PropertyType)
                    throw new ArgumentException(string.Format("The rank of the properties in a given type must be contiguous - you cannot have t1.p1-rank=1, t2.p2-rank=2, t1.p3-rank=3 - see property {0} in type {1}", orderedProperty.Name, orderedProperty.PropertyType.Name));

            // Return a list sorted in ascending order by Rank...
            return orderedProperties;
        }

        internal static bool ValueType(Type type)
        {
            Type actualType = GetActualType(type);
            if (actualType.IsEnum)
                return true;
            if (actualType.Name == "String")
                return true;
            if (actualType.Name == "Color")
                return true;
            if (actualType.Name == "Uri")
                return true;
            if (actualType.Name == "TimeZoneInfo")
                return true;
            if (actualType.GetInterface("IEnumerable") != null)
                return false;
            return actualType.IsValueType;
        }

        private const string CSharpGetValueLiteral =
            // {0} - target type name
            // {1} - properties 
            // {2} - return external id if there is one if it's asked for
            // {3} - MakeExternalIDForGetValueGetSubtypes()
@"        public static object GetValueForPath(string[] path, int index, {0} item)
        {{
            if (item == null)
                return null;
{2}
{3}
            switch (path[index])
            {{
{1}
                default:
                    return null;
            }}

            // Not required as all previous paths get us out of here
            // return null;
        }}
";

        private const string CSharpForExternalIdForGetValueGetSubtypesLiteral = 
            // {0} - full subtype name
            // {1} - type serializer name
            @"            if (item is {0})
                return {1}.GetValueForPath(path, index, ({0})item);
";

        private const string CSharpGetExternalIdForGetValueLiteral =
            @"            if (index == 0)
                switch (path[0])
                {
                    case ""ExternalID"":
                        return GetExternalIDForObject(item);
                }

";

        private const string CSharpGetSimpleValueLiteral = 
@"                case ""{0}"":
                    return item.{0};
";

        private const string CSharpGetCompoundValueLiteral =
@"                case ""{0}"":
                    return CIP4.Serializer.{1}_Serializer.GetValueForPath(path, index + 1, item.{0});
";

        private const string CSharpMvUnsupportedLiteral =
@"                case ""{1}"":
                    throw new NotImplementedException(""{0} properties are not supported in expressions at this time: {1}"");
";

        protected string MakeGetValue()
        {
            string text = string.Empty;

            foreach (PropertyInfo property in WrapperSerializerType.SortedProperties(TheType.TheType.GetProperties()).Values)
            {
                CIP4ExternalIDAttribute extIdAtt = CIP4ExternalIDAttribute.GetAttribute(property);
            
                if (!IncludeThisProperty(property) && extIdAtt == null)
                    continue;
                //if (property.PropertyType.GetInterface("IList`1") != null)
                //    text += string.Format(CSharpMvUnsupportedLiteral, "IList", property.Name);
                else if (property.PropertyType.GetInterface("IDictionary") != null)
                    text += string.Format(CSharpMvUnsupportedLiteral, "Dictionary", property.Name);
                else
                    text += SerializeSv(property);
            }

            return string.Format(CSharpGetValueLiteral, 
                TheType.TheTypeFullName, 
                text, 
                GetExternalIdProperties(TheType).Count > 0? CSharpGetExternalIdForGetValueLiteral: string.Empty,
                MakeExternalIdForGetValueGetSubtypes());
        }

        private string MakeExternalIdForGetValueGetSubtypes()
        {
            string text = string.Empty;
            foreach (Type t in TheType.TheType.Assembly.GetTypes())
            {
                if (!TheType.TheType.IsAssignableFrom(t) || t == TheType.TheType)
                    continue;
                TypeContextType tc = new TypeContextType { OriginalType = t };
                text += string.Format(CSharpForExternalIdForGetValueGetSubtypesLiteral,
                    tc.TheTypeFullName,
                    tc.TypeSerializerName);
                if (!UsedTypes.ContainsKey(tc.TheTypeFullName))
                    UsedTypes.Add(tc.TheTypeFullName, tc);
            }
            return text;
        }

        private string SerializeSv(PropertyInfo property)
        {
            TypeContextType propType = new TypeContextType { OriginalType = property.PropertyType };
            if (propType.TheType == typeof(Stream))
                return string.Format(CSharpMvUnsupportedLiteral, "Stream", property.Name);
            if (propType.HostTypeName == "CIP4ReferenceType")
                return string.Format(CSharpMvUnsupportedLiteral, "CIP4ReferenceType", property.Name);
            //if (propType.OriginalType.IsGenericType)
            //    return string.Format(CSharpMvUnsupportedLiteral, "Generic type", property.Name);
            //if (propType.TheType == typeof(object))
            //    return string.Format(CSharpMvUnsupportedLiteral, "object", property.Name);
            if (ValueType(propType.TheType))
                return string.Format(CSharpGetSimpleValueLiteral, property.Name);
            if (IncludeThisType(property.PropertyType))
                return string.Format(CSharpGetCompoundValueLiteral, property.Name, 
                    new TypeContextType(property.PropertyType).TheTypeName);
            return string.Empty;
        }

        private const string CSharpGetKeysLiteral =
            @"        private static List<KeyValuePair<string, int>> _Keys;
        public static IEnumerable<KeyValuePair<string, int>> GetKeys()
        {{
            if (_Keys == null)
            {{
                _Keys = new List<KeyValuePair<string, int>>
                {{
{0}
                }};
            }}
            return _Keys;
        }}

";

        private const string CSharpMakeKeyLiteral = 
            // {0} - key property name
            // {1} - index number - 0 is the ExternalID index
            @"                    new KeyValuePair<string, int>(""{0}"", {1}),
";

        protected string MakeKeys()
        {
            string text = string.Empty;

            List<PropertyInfo> externalIds = GetExternalIdProperties(TheType);
            foreach (PropertyInfo prop in externalIds)
                text += string.Format(CSharpMakeKeyLiteral, prop.Name, 0);

            int index = 1;
            foreach (PropertyInfo prop in TheType.TheType.GetProperties())
            {
                if (externalIds.Contains(prop))
                    continue;
                CIP4PropertyAttribute propAtt = CIP4PropertyAttribute.GetAttribute(prop);
                if (propAtt != null && propAtt.IsIndexed)
                    text += string.Format(CSharpMakeKeyLiteral, prop.Name, index++);
            }

            return string.Format(CSharpGetKeysLiteral, text);
        }
        
        private const string CSharpGetItemVersionBodyLiteral =
@"
            if (previousVersion == null || previousVersion.{0} == null || !previousVersion.{0}.Versions.ContainsKey(versionTimestamp))
                return previousVersion;
            return GetItemVersion(versionTimestamp, previousVersion, previousVersion.{0}.Versions[versionTimestamp].Values);
";

        protected string GetItemVersionBody()
        {
            if (VersionProperty == null)
                return "            return previousVersion;";

            return string.Format(CSharpGetItemVersionBodyLiteral,
                VersionProperty.Name);
        }


        // (0) - type name
        // (1) - version property name
        // {2} - ".Ticks" if the proeprty is a DateTime, string.Empty otherwise
        // (3) - either "version" if it's numeric or "new DateTime(version)" if it's a DateTime
        private const string CSharpForCheckVersionLiteral =
            @"
        public bool CheckVersion {{ get {{ return false; }} }}

        public static string GetVersionPropertyPath() {{ return ""{2}.{3}""; }}

        public static Int64 GetVersionNumber(object item)
        {{
            return (({0})item).{1};
        }}

        public static void IncrementVersionNumber(object item)
        {{
            (({0})item).{1} += 1;
        }}
";

        protected string MakeCheckVersion()
        {
            PropertyInfo prop = GetVersionProperty();
            if (prop != null)
            {
                if (!(prop.PropertyType == typeof(int) || prop.PropertyType == typeof(Int64)))
                    throw new ArgumentException(string.Format("ItemVersion properties must be Int64 or int: {0}.{1}", prop.DeclaringType.FullName, prop.Name));
                return string.Format(CSharpForCheckVersionLiteral, TheType.TheTypeFullName, prop.Name, TheType.SerializeAs, GetSerializeAs(prop));
            }
            return string.Empty;
        }

        protected PropertyInfo GetVersionProperty()
        {
            PropertyInfo property = TheType.TheType.GetProperties().FirstOrDefault(prop => CIP4ItemVersionAttribute.GetAttribute(prop) != null);

            if (property != null && !(property.PropertyType == typeof(Int64) || property.PropertyType == typeof(int)))
            {
                throw new ArgumentException(string.Format("Version proeprties must be of type int or Int64: {0}", property.Name));
            }

            return property;
        }

        protected PropertyInfo GetTimestampProperty()
        {
            return TheType.TheType.GetProperties().FirstOrDefault(prop => CIP4ItemModifiedTimestampAttribute.GetAttribute(prop) != null);
        }


        private const string CSharpIncrementVersionNumberLiteral =
            @"
            IncrementVersionNumber(value);";

        private const string CSharpSetModifiedDate =
            @"
            value.{0} = DateTime.Now;";

        protected string GetSerializeIncrementVersionNumber()
        {
            string text = string.Empty;

            if (GetVersionProperty() != null)
                text = CSharpIncrementVersionNumberLiteral;

            PropertyInfo prop = GetTimestampProperty();

            if (prop != null)
                text += string.Format(CSharpSetModifiedDate,
                    prop.Name);

            return text;
        }

        private const string CSharpVersionSvLiteral =
@"
                if (value.{1} != null && value.{0} != (({2})value.{1}.GetCurrent<{2}>()).{0})
                    value.{1}.AddValue(""{3}"", (({2})value.{1}.GetCurrent<{2}>()).{0});
";

        private const string CSharpVersionSvNullableLiteral =
@"
                if (value.{1} != null && value.{0}.HasValue && value.{0} != (({2})value.{1}.GetCurrent<{2}>()).{0})
                    value.{1}.AddValue(""{3}"", (({2})value.{1}.GetCurrent<{2}>()).{0});
";

        protected string SerializeSimpleVersions(PropertyInfo property)
        {
            TypeContextType actualType = new TypeContextType { OriginalType = property.PropertyType };
            PropertyInfo versionsProp = VersionProperty;
            if (versionsProp != null)
            {
                if (actualType.HostTypeName == "Nullable")
                    return string.Format(CSharpVersionSvNullableLiteral,
                        property.Name,
                        versionsProp.Name,
                        TheType.TheTypeFullName,
                        GetSerializeAs(property));
                return string.Format(CSharpVersionSvLiteral,
                        property.Name,
                        versionsProp.Name,
                        TheType.TheTypeFullName,
                        GetSerializeAs(property));
            }
            return string.Empty;
        }

        // {0} - property name
        // {1} - qualified name of the type that owns the property
        // {2} - version property name
        // {3} - property type
        private const string CSharpEmptySvNullPropertyVersionLiteral =
            @"
            else if (value.{2} != null && value.{2}.GetCurrent<{1}>().{0} != default({3}))
            {{
                value.{2}.AddValue(""{0}"", value.{2}.GetCurrent<{1}>().{0});
            }}
";

        // {0} - property name
        // {1} - qualified name of the type that owns the property
        // {2} - version property name
        // {3} - property type
        private const string CSharpEmptySvNullNullablePropertyVersionLiteral =
            @"
            else if (value.{2} != null && value.{2}.GetCurrent<{1}>().{0} != null)
            {{
                value.{2}.AddValue(""{0}"", value.{2}.GetCurrent<{1}>().{0});
            }}
";

        protected string SerializeSvNullVersion(PropertyInfo property)
        {
            if (VersionProperty == null)
                return string.Empty;
            TypeContextType actualType = new TypeContextType(property.PropertyType);
            if (actualType.HostTypeName == "Nullable")
                return string.Format(CSharpEmptySvNullNullablePropertyVersionLiteral,
                    property.Name,
                    TheType.TheTypeFullName,
                    VersionProperty.Name);
            return string.Format(CSharpEmptySvNullPropertyVersionLiteral,
                property.Name,
                TheType.TheTypeFullName,
                VersionProperty.Name,
                actualType.TheTypeFullName);
        }


        protected static string GetIEqualityComparer(TypeContextType actualType)
        {
            if (actualType.TheType == typeof(string))
                return "StringComparer.CurrentCulture";
            if (ValueType(actualType.TheType))
                return string.Format("new EqualityComparer<{0}{1}>((x, y) => x == y, x => x.GetHashCode())", actualType.TheTypeFullName, actualType.IsNullable? "?": string.Empty);
            return string.Format("new {0}.EqualityComparer()", actualType.TypeSerializerName);
        }

        private const string CSharpGetSerializeAsNameLiteral =
            @"
        public static string GetSerializeAsName(string fullName)
        {{
            string lowerName = fullName.ToLower();
{0}
            return fullName;
        }}
";


        protected string GetSerializeAsNameText()
        {
            HashSet<string> recursionDetector = new HashSet<string>();
            HashSet<string> duplicatesDetector = new HashSet<string>();
            StringBuilder text = new StringBuilder();
            GetSerializeAsNameEntries(
                TheType, 
                TheType.TheTypeName + ".", 
                TheType.SerializeAs + ".", 
                text,
                recursionDetector,
                duplicatesDetector);
            text.Append(string.Format(CSharpGetSerializeAsEntryLiteral, TheType.TheTypeName.ToLower(), TheType.SerializeAs));
            return string.Format(CSharpGetSerializeAsNameLiteral, text);
        }

        private const string CSharpGetSerializeAsEntryLiteral =
            @"
            if (lowerName == ""{0}"") return ""{1}"";";

        protected void GetSerializeAsNameEntries(TypeContextType forType, string path, string asPath, StringBuilder text, HashSet<string> recursionDetector, HashSet<string> duplicatesDetector)
        {
            foreach (PropertyInfo property in forType.TheType.GetProperties())
            {
                if (!IncludeThisProperty(property))
                    continue;
                if (property.GetSetMethod(true) == null)
                    continue;

                CIP4PropertyAttribute att = CIP4PropertyAttribute.GetAttribute(property);

                string serializeAsName = att == null || string.IsNullOrEmpty(att.SerializeAs) ? property.Name : att.SerializeAs;

                text.Append(string.Format(CSharpGetSerializeAsEntryLiteral,
                    (path + property.Name).ToLower(), 
                    asPath + serializeAsName));

                if (duplicatesDetector.Contains(asPath + serializeAsName))
                    throw new ArgumentException(string.Format("SerializeAs names must be unique - see: {0}",asPath + serializeAsName));

                duplicatesDetector.Add(asPath + serializeAsName);

                Type propertyType = GetActualType(property.PropertyType);

                CIP4ItemAttribute itAtt = CIP4ItemAttribute.GetAttribute(propertyType);

                if (itAtt != null || (att != null && att.SerializeTarget))
                {
                    TypeContextType propType = new TypeContextType(property.PropertyType);
                    if (recursionDetector.Contains(propType.TheTypeFullName))
                        continue;
                    recursionDetector.Add(propType.TheTypeFullName);
                    GetSerializeAsNameEntries(
                        propType,
                        path + property.Name + "." + propertyType.Name + ".",
                        asPath + serializeAsName + "." + propType.SerializeAs + ".",
                        text,
                        recursionDetector,
                        duplicatesDetector);

                    recursionDetector.Remove(propType.TheTypeFullName);
                }
            }
        }


    }
}
